package com.nulldev.util.web.HttpServer3.extensions.http2.compat;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.StringTokenizer;

import com.nulldev.util.web.HttpServer3.extensions.http2.errors.CannotParseRequestException;
import com.nulldev.util.web.HttpServer3.extensions.http2.errors.UnsupportedHttpMethodException;
import com.nulldev.util.web.HttpServer3.extensions.http2.util.ReadUntilResult;
import com.nulldev.util.web.HttpServer3.extensions.http2.util.TextBinaryInputStream;
import com.nulldev.util.web.HttpServer3.util.HttpServerUtil;

import sun.misc.IOUtils;

/**
 * The class names used in request.* package is mostly based on
 * https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages
 */
public class RequestParser {
	/**
	 * Parse an http request. Please also note that as soon as an error is catched,
	 * the HttpRequest immediately becomes invalid. Use
	 * {@link HttpRequest#isValid()} to see if it is still valid or not.
	 * 
	 * @param is An {@link InputStream} which is not previously read or modified.
	 * @return A parsed http {@link com.vincentcodes.webserver.request.HttpRequest
	 *         request}.
	 */
	public static HttpRequest parse(InputStream is) {
		TextBinaryInputStream reader = new TextBinaryInputStream(is);

		HttpRequest request = new HttpRequest();
		HttpHeaders headers = new HttpHeaders();
		HttpRequestBasicInfo basicInfo = null;
		MultipartFormData multipart = null;
		HttpBody body = new HttpBodyStream();

		// For the purpose of logging the whole request
		// (Necessary? No, maybe I'll add an option to disable it)
		StringBuilder wholeRequest = new StringBuilder();
		try {
			boolean requestBodyIncoming = false;
			String line = null;

			line = reader.readLine();
			wholeRequest.append(line).append("\r\n");
			basicInfo = parseFirstLine(line);

			// Add a little decoded text
			// if(line.contains("%")){
			// wholeRequest.append("('").append(basicInfo.getPath()).append("')\n");
			// }

			while ((line = reader.readLine()) != null) {
				wholeRequest.append(line).append("\r\n");
				if (line.isEmpty()) {
					requestBodyIncoming = true;
				}

				if (!requestBodyIncoming) {
					String key = line.substring(0, line.indexOf(':')).trim();
					String val = line.substring(line.indexOf(':') + 1).trim();
					headers.add(key, val);
				} else {
					EntityInfo entityInfo = headers.getEntityInfo();
					int length = entityInfo.getLength();

					if (length > 0) {
						if (entityInfo.getType().equals("multipart/form-data")) {
							multipart = parseMultipartFormData(reader, entityInfo, wholeRequest);
							if (multipart == null)
								request.invalid();
						} else {
							// 64MB -> use tmp file
							if (length > 67108864) {
								body = new HttpBodyFileStream();
								int sizeRead = 0;
								byte[] content = new byte[4096];
								while ((sizeRead = reader.read(content)) != -1) {
									// wholeRequest.append(new String(content)).append('\n');
									body.writeToBody(content, sizeRead);
								}
							} else {
								body.writeToBody(IOUtils.readExactlyNBytes(reader, length)); // reader.readNBytes(length)
							}
						}
					} else if (basicInfo.getMethod().equals("PRI")) {
						// https://tools.ietf.org/html/rfc7540#section-3.5
						reader.readLine(); // Skip "SM\r\n"
						reader.readLine(); // Skip "\r\n"
						wholeRequest.append("SM\r\n\r\n");
					}
					break;
				}
			}

			request.setHeaders(headers);
			request.setBody(body);
			request.setBasicInfo(basicInfo);
			if (multipart != null) {
				request.setMultipartFormData(multipart);
			}
		} catch (IOException e) {
			HttpServerUtil.logger.error("Catching a " + e.getClass().getName() + ": " + e.getMessage());
			// An established connection was aborted by the software in your host machine
			if (!HttpServerUtil.isIgnorableException(e)) {
				e.printStackTrace();
				request.invalid();
			}
		} catch (Exception e) {
			HttpServerUtil.logger.error("Catching a " + e.getClass().getName() + ": " + e.getMessage());
			e.printStackTrace();
			// request.invalid();
			// throw new CannotParseRequestException("An error occured while parsing an Http
			// Request", e);
		}
		request.setWholeRequest(wholeRequest.toString());
		if (request.getWholeRequest().startsWith("PRI")) {
			if (HttpServerUtil.lowLevelDebugMode)
				HttpServerUtil.logger.debug("HTTP2 upgrade sequence received");
			return request;
		}
		HttpServerUtil.logger.debug("\n" + request.getWholeRequest());
		return request;
	}

	/**
	 * Similar to {@link RequestParser#parse(InputStream)}
	 */
	public static HttpRequest parse(String wholeRequest) {
		return parse(new ByteArrayInputStream(wholeRequest.getBytes()));
	}

	/**
	 * @param reader       [required]
	 * @param entityInfo   [required]
	 * @param wholeRequest [required]
	 * @return null if it has no boundary
	 */
	public static MultipartFormData parseMultipartFormData(TextBinaryInputStream reader, EntityInfo entityInfo, StringBuilder wholeRequest) throws IOException {
		MultipartFormData multipart = new MultipartFormData();
		HttpHeaders headers = new HttpHeaders();
		String boundary = entityInfo.getBoundary();
		if (boundary == null) {
			return null;
		}

		String line;
		boolean startBody = false;
		while ((line = reader.readLine()) != null) {
			if (line.equals("--" + boundary)) {
				wholeRequest.append("--" + boundary).append("\n");
				headers = new HttpHeaders();
				startBody = false;
				continue;
			}

			if (line.isEmpty()) {
				wholeRequest.append("\n");
				startBody = true;
			} else if (!startBody) {
				wholeRequest.append(line).append("\n");

				String key = line.substring(0, line.indexOf(':')).trim();
				String val = line.substring(line.indexOf(':') + 1).trim();
				headers.add(key, val);
			}

			if (startBody) {
				HttpBody body = new HttpBodyFileStream();
				ReadUntilResult result = reader.readBytesUntil("\r\n--" + boundary, 4096);
				while (result.data() != null) {
					body.writeToBody(result.data());
					if (result.isMatchingStrFound())
						break;
					result = reader.readBytesUntil("\r\n--" + boundary, 4096);
				}

				// wholeRequest.append(body.string()).append("\n");
				wholeRequest.append("--" + boundary);
				wholeRequest.append(reader.readLine()).append("\n"); // skip the "\n" or "--\n"
				// WebServer.logger.warn(wholeRequest.toString());
				multipart.put(headers, body);
				startBody = false;
				String endString = "--" + boundary + "--";
				if (wholeRequest.substring(wholeRequest.length() - endString.length() - 2).lastIndexOf(endString) != -1)
					break;
			}
		}
		return multipart;
	}

	/**
	 * @param line first line of an http request (eg. "GET /file HTTP/1.1")
	 * @return StartLine if nothing went wrong. null if the request method is not
	 *         supported or in an invalid format
	 */
	public static HttpRequestBasicInfo parseFirstLine(String line) throws CannotParseRequestException {
		String method = line.substring(0, line.indexOf(' ')); // till the 1st space
		String rawPath = line.substring(line.indexOf(' ') + 1, line.lastIndexOf(' ')); // the middle part
		String version = line.substring(line.lastIndexOf(' ') + 1); // the last part
		if (!HttpRequest.SUPPORTED_METHODS.contains(method)) {
			throw new UnsupportedHttpMethodException("Method " + method + " is not supported.");
		}

		String path = cleanUpPath(rawPath);
		HashMap<String, String> args = parseParametersSafe(path);

		if (args.size() > 0) {
			path = path.substring(0, path.indexOf('?'));
			try {
				path = URLDecoder.decode(path, StandardCharsets.UTF_8.name());
			} catch (UnsupportedEncodingException e) {
				throw new RuntimeException(e);
			}
			return new HttpRequestBasicInfo(method, path, version, args);
		}
		try {
			path = URLDecoder.decode(path, StandardCharsets.UTF_8.name());
		} catch (UnsupportedEncodingException e) {
			throw new RuntimeException(e);
		}
		return new HttpRequestBasicInfo(method, path, version);
	}

	/**
	 * @param path relative path of a URI (eg. /path/to/file?param=value)
	 */
	private static String cleanUpPath(String path) {
		if (path.indexOf('?') == -1) {
			while (path.contains("//"))
				path = path.replaceAll("//", "/");
		} else {
			StringTokenizer pathTokenizer = new StringTokenizer(path, "?", false);
			String url = pathTokenizer.nextToken();
			while (url.contains("//"))
				url = url.replaceAll("//", "/");
			path = String.join("?", url, pathTokenizer.nextToken());
		}
		return path;
	}

	private static HashMap<String, String> parseParametersSafe(String path) {
		int indexQuestionMark = path.indexOf('?');
		if (indexQuestionMark == -1 || indexQuestionMark >= path.length() - 1) {
			return new HashMap<>();
		}
		String strArgs = path.substring(indexQuestionMark + 1);
		return parseParameters(strArgs);
	}

	private static HashMap<String, String> parseParameters(String strArgs) {
		HashMap<String, String> result = new HashMap<>();
		if (strArgs.indexOf('&') != -1) {
			String[] args = strArgs.split("&");
			for (String arg : args) {
				if (arg.indexOf('=') == -1) {
					if (!arg.trim().equals(""))
						result.put(arg, "");
				} else {
					try {
						String[] splited = arg.split("=");
						if (splited.length == 2)
							result.put(splited[0], URLDecoder.decode(splited[1], "utf-8"));
						else
							result.put(splited[0], "");
					} catch (UnsupportedEncodingException ignored) {
					}
				}
			}
		} else {
			if (strArgs.indexOf('=') == -1) {
				if (!strArgs.trim().equals(""))
					result.put(strArgs, "");
			} else {
				try {
					String[] splited = strArgs.split("=");
					if (splited.length == 2)
						result.put(splited[0], URLDecoder.decode(splited[1], "utf-8"));
					else
						result.put(splited[0], "");
				} catch (UnsupportedEncodingException ignored) {
				}
			}
		}
		return result;
	}
}
